package android.androidVNC;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;

import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;

import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import android.view.KeyEvent;




public class VNC {
	private RfbProto rfb;
	private String host;
	private int port;
	private String username;
	private String password;
	private FTPClient ftp;
	private FTPClientConfig config;

	// Color Model settings
	private COLORMODEL pendingColorModel = COLORMODEL.C24bit;
	private COLORMODEL colorModel = null;
	private int bytesPerPixel = 0;
	private int[] colorPalette = null;
	private int preferredEncoding = -1;

	//info for knowing what elements to update in the GUI
	private boolean updated = false;
	private int[] updateRectCoord = new int[4];

	//thread safety
	private Semaphore bitmapKey = new Semaphore(1);

	//desktop image data
	private int[] pixels = null;
	private Bitmap bmp = null;

	//stuff for trying to maintain only the pieces of the desktop we need
	private List<BitmapElement> bmpElements = new ArrayList<BitmapElement>();

	private class BitmapElement{
		public Bitmap bmp = null;
		public int x = 0;
		public int y = 0;
		public int w = 0;
		public int h = 0;

		public BitmapElement(Bitmap bitmap, int xVal, int yVal, int wVal, int hVal){
			bmp = bitmap;
			x = xVal;
			y = yVal;
			w = wVal;
			h = hVal;
		}
	}

	public VNC(String host, int port, String username, String password) throws IOException{
		this.host = host;
		this.port = port;
		this.username = username;
		this.password = password;
	}

	public boolean connectToVNC() throws Exception{
		Log.d("msg", "attempting to initialize rfb");
		rfb = new RfbProto(this.host, this.port);
		Log.d("msg", "rfb has been initialized");
		doHandshaking();
		Log.d("msg","Handshaking handled sucessfully");
		doProtocolInit();
		return true;
	}

	private boolean doHandshaking() throws Exception{
		rfb.readVersionMsg();
		rfb.writeVersionMsg();
		int bitPref = 0;
		int secType = rfb.negotiateSecurity(bitPref);
		int authType;
		if(secType == RfbProto.SecTypeTight) {
			rfb.initCapabilities();
			rfb.setupTunneling();
			authType = rfb.negotiateAuthenticationTight();
		} else if (secType == RfbProto.SecTypeUltra34) {
			rfb.prepareDH();
			authType = RfbProto.AuthUltra;
		} else {
			authType = secType;
		}

		switch (authType) {
		case RfbProto.AuthNone:
			rfb.authenticateNone();
			break;
		case RfbProto.AuthVNC:
			rfb.authenticateVNC(password);
			break;
		case RfbProto.AuthUltra:
			rfb.authenticateDH(username,password);
			break;
		default:
			throw new Exception("Unknown authentication scheme " + authType);
		}

		return true;
	}

	private void doProtocolInit() throws IOException{
		rfb.writeClientInit();
		rfb.readServerInit();
		setPixelFormat();
	}

	private void setPixelFormat() throws IOException{
		pendingColorModel.setPixelFormat(rfb);
		bytesPerPixel = pendingColorModel.bpp();
		colorPalette = pendingColorModel.palette();
		colorModel = pendingColorModel;
		pendingColorModel = null;
		Log.d("msg","bytes per pixel: " + bytesPerPixel);
		Log.d("msg", "width: " + rfb.framebufferWidth + " height: " + rfb.framebufferHeight);
		pixels = new int[rfb.framebufferWidth*rfb.framebufferHeight];
		bmp = Bitmap.createBitmap(rfb.framebufferWidth, rfb.framebufferHeight, Bitmap.Config.RGB_565);
	}

	public void keyEvent(int keyCode, KeyEvent evt, boolean down) throws IOException{
		int key;
		int metaState = evt.getMetaState();

		switch(keyCode) {
		case KeyEvent.KEYCODE_BACK :        key = 0xff1b; break;
		case KeyEvent.KEYCODE_DPAD_LEFT:    key = 0xff51; break;
		case KeyEvent.KEYCODE_DPAD_UP:      key = 0xff52; break;
		case KeyEvent.KEYCODE_DPAD_RIGHT:   key = 0xff53; break;
		case KeyEvent.KEYCODE_DPAD_DOWN:    key = 0xff54; break;
		case KeyEvent.KEYCODE_DEL:          key = 0xff08; break;
		case KeyEvent.KEYCODE_ENTER:        key = 0xff0d; break;
		case KeyEvent.KEYCODE_DPAD_CENTER:  key = 0xff0d; break;
		default:                                                    
			key = evt.getUnicodeChar();
			metaState = 0;
			break;
		}
		rfb.writeKeyEvent(key, metaState, down);
	}

	public void sendMouseEvent(int x, int y, int modifiers, int pointerMask) throws IOException{
		rfb.writePointerEvent(x, y, modifiers, pointerMask);
	}

	public boolean hasConnection(){
		return rfb.closed();
	}

	public void processNormalProtocol() throws Exception
	{
		boolean fullUpdateNeeded = true;
		//get the first frame
		writeFullUpdateRequest(false);

		while(!rfb.closed()){
			if(rfb.is.available() > 0){
				int msgType = rfb.readServerMessageType();

				switch(msgType){

				//the message is a framebuffer update
				case RfbProto.FramebufferUpdate:
					rfb.readFramebufferUpdate();

					Log.d("msg","number of rectangles is: " + rfb.updateNRects);

					for(int i = 0; i < rfb.updateNRects; i++){
						//read the rectangle header and get x, y, w, h
						rfb.readFramebufferUpdateRectHdr();
						int rx = rfb.updateRectX, ry = rfb.updateRectY;
						int rw = rfb.updateRectW, rh = rfb.updateRectH;

						if (rfb.updateRectEncoding == RfbProto.EncodingLastRect) {
							Log.v("msg", "rfb.EncodingLastRect");
							break;
						}

						if (rfb.updateRectEncoding == RfbProto.EncodingNewFBSize) {
							rfb.setFramebufferSize(rw, rh);
							// - updateFramebufferSize();
							Log.v("msg", "rfb.EncodingNewFBSize");
							break;
						}

						if (rfb.updateRectEncoding == RfbProto.EncodingXCursor || rfb.updateRectEncoding == RfbProto.EncodingRichCursor) {
							// - handleCursorShapeUpdate(rfb.updateRectEncoding,
							// rx,
							// ry, rw, rh);
							Log.v("msg", "rfb.EncodingCursor");
							continue;

						}

						if (rfb.updateRectEncoding == RfbProto.EncodingPointerPos) {
							// This never actually happens
							Log.v("msg", "rfb.EncodingPointerPos");
							continue;
						}

						rfb.startTiming();
						switch (rfb.updateRectEncoding) {
						case RfbProto.EncodingRaw:
							Log.d("msg","width: " + rw + " height: " + rh);
							handleRawRect(rx, ry, rw, rh);
							break;
						case RfbProto.EncodingCopyRect:
							//handleCopyRect(rx, ry, rw, rh);
							//Log.v(TAG, "CopyRect is Buggy!");
							break;
						case RfbProto.EncodingRRE:
							//handleRRERect(rx, ry, rw, rh);
							break;
						case RfbProto.EncodingCoRRE:
							//handleCoRRERect(rx, ry, rw, rh);
							break;
						case RfbProto.EncodingHextile:
							//handleHextileRect(rx, ry, rw, rh);
							break;
						case RfbProto.EncodingZRLE:
							//handleZRLERect(rx, ry, rw, rh);
							break;
						case RfbProto.EncodingZlib:
							//handleZlibRect(rx, ry, rw, rh);
							break;
						default:
							Log.e("msg", "Unknown RFB rectangle encoding " + rfb.updateRectEncoding + " (0x" + Integer.toHexString(rfb.updateRectEncoding) + ")");
						}

						rfb.stopTiming();
					}

					writeFullUpdateRequest(true);
					break;

				default:
					Log.d("msg","got a server message I don't know what to do with");
				}
			} else {
				writeFullUpdateRequest(true);
			}
		}

	}

	private byte[] handleRawRectBuffer = new byte[128];
	private void handleRawRect(int x, int y, int w, int h) throws IOException{
		if (bytesPerPixel == 1) {
			// 1 byte per pixel. Use palette lookup table.
			if (w > handleRawRectBuffer.length) {
				handleRawRectBuffer = new byte[w];
			}
			int i;
			for (int dy = y; dy < y +h; dy++) {
				rfb.readFully(handleRawRectBuffer, 0, w);
				for (i = 0; i < w; i++) {
					pixels[0 + i] = colorPalette[0xFF & handleRawRectBuffer[i]];
				}
			}
		} else {
			// 4 bytes per pixel (argb) 24-bit color
			final int l = w * 4;
			if (l>handleRawRectBuffer.length) {
				handleRawRectBuffer = new byte[l];
			}
			int i, offset;
			for (int dy = y; dy < y + h; dy++) {
				rfb.readFully(handleRawRectBuffer, 0, l);
				offset = offset(x, dy);
				for (i = 0; i < w; i++) {
					final int idx = i*4;
					pixels[offset + i] = // 0xFF << 24 |
							(handleRawRectBuffer[idx + 2] & 0xff) << 16 | (handleRawRectBuffer[idx + 1] & 0xff) << 8 | (handleRawRectBuffer[idx] & 0xff);
				}
			}
		}

		//update the bitmap
		try {
			bitmapKey.acquire();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		bmp.setPixels(pixels, 0, rfb.framebufferWidth, 0, 0, rfb.framebufferWidth, rfb.framebufferHeight);

		//set flags so trout knows what has been updated
		updated = true;
		updateRectCoord[0] = x;
		updateRectCoord[1] = y;
		updateRectCoord[2] = w;
		updateRectCoord[3] = h;

		bitmapKey.release();
	}

	private int offset(int x, int y){
		return y * rfb.framebufferWidth + x;
	}

	public Bitmap getBitmap(){
		Bitmap bitmap = null;

		try {
			bitmapKey.acquire();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		bitmap = bmp;
		bitmapKey.release();

		return bitmap;
	}

	public void addBmpElement(int x, int y, int w, int h){
		BitmapElement bmp = new BitmapElement(Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565), x, y, w, h);
		bmpElements.add(bmp);
	}

	public boolean connectToFTP(){
		ftp = new FTPClient();
		config = new FTPClientConfig();
		ftp.configure(config);
		boolean noError = true;
		try{
			int reply;
			ftp.connect(host);
			ftp.login(username,password);
			ftp.enterLocalPassiveMode();
			Log.d("msg","Connected to " + host + ".");
			Log.d("msg",ftp.getReplyString());

			//after connection attempt, you should check the reply code to verify success
			reply = ftp.getReplyCode();

			if(!FTPReply.isPositiveCompletion(reply)){
				ftp.disconnect();
				Log.d("msg","FTP server refused connection.");
			}
		} catch(IOException e){
			noError = false;
			Log.d("msg", e.toString());
		}
		return noError;
	}

	/**
	 * Uploads a file to the ftp server
	 * @param filename the name of the file (as it will appear on the server)
	 * @param filepath the location of the file on the device
	 * @return true if error free, false otherwise
	 */
	public boolean uploadFTPFile(String filename, String filepath){
		boolean noError = true;
		try{

			//create a new input stream for the file that will be uploaded
			InputStream is = new FileInputStream(filepath);

			//store the file on the server using the given name and input stream
			ftp.storeFile(filename,is);

			Log.d("msg","File uploaded successfully?");

		} catch(IOException e){
			Log.d("msg", e.toString());
			noError = false;
		}
		return noError;
	}

	/**
	 * "Downloads" a file from the ftp server
	 * @param filename the name of the file to download
	 * @return a BufferedReader object containing the file from the ftp server
	 */
	public BufferedReader downloadFTPFile(String filename){
		BufferedReader br = null;
		try{
			InputStream is = ftp.retrieveFileStream(filename);
			br = new BufferedReader(new InputStreamReader(is));
		} catch(IOException e){
			Log.d("msg",e.toString());
		} catch(Exception e){
			Log.d("msg", e.toString());
		}

		return br;
	}

	public String[] listAvailableFiles(){
		String[] fileNames = null;
		boolean error = false;
		FTPFile[] ftpFiles = null;
		try{
			ftpFiles = ftp.listFiles();
		} catch (IOException e){
			Log.d("msg", e.toString());
			error = true;
		}

		//get just a string of filenames for trout
		if(!error){
			fileNames = new String[ftpFiles.length];
			for(int i = 0; i < ftpFiles.length; i++){
				fileNames[i] = ftpFiles[i].getName();
			}
		}

		return fileNames;
	}

	public void closeFTPCConnection(){
		try{
			ftp.logout();
		} catch(IOException e) {
			Log.d("msg", e.toString());
		} finally {
			try{
				ftp.disconnect();
			} catch(IOException e) {
				Log.d("msg", e.toString());
			}
		}
	}

	private void writeFullUpdateRequest(boolean incremental) throws IOException{
		rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth, rfb.framebufferHeight, incremental);
	}

	private void setEncodings(boolean autoSelectOnly) {
		if (rfb == null || !rfb.inNormalProtocol)
			return;

		if (preferredEncoding == -1) {
			// Preferred format is ZRLE
			preferredEncoding = RfbProto.EncodingZRLE;
		} else {
			// Auto encoder selection is not enabled.
			if (autoSelectOnly)
				return;
		}

		int[] encodings = new int[20];
		int nEncodings = 0;

		encodings[nEncodings++] = preferredEncoding;

		// if (preferredEncoding != RfbProto.EncodingTight)
		// encodings[nEncodings++] = RfbProto.EncodingTight;
		if (preferredEncoding != RfbProto.EncodingZRLE)
			encodings[nEncodings++] = RfbProto.EncodingZRLE;
		if (preferredEncoding != RfbProto.EncodingHextile)
			encodings[nEncodings++] = RfbProto.EncodingHextile;
		if (preferredEncoding != RfbProto.EncodingZlib)
			encodings[nEncodings++] = RfbProto.EncodingZlib;
		if (preferredEncoding != RfbProto.EncodingCoRRE)
			encodings[nEncodings++] = RfbProto.EncodingCoRRE;
		if (preferredEncoding != RfbProto.EncodingRRE)
			encodings[nEncodings++] = RfbProto.EncodingRRE;

	}

	public boolean hasUpdated(){
		return updated;
	}

	public void ackUpdate(){
		updated = false;
	}

	public int[] updatedCoordinates(){
		return updateRectCoord;
	}

	// values useful for launching 
	final static int CTRL = 0xffe3;
	final static int ESC = 0xff1b;
	final static int ENTER = 0xff0d;
	public void launchApplication(String name){
		if(name != null){
			try {
				rfb.writeKeyEvent(CTRL, 0, true);
				rfb.writeKeyEvent(ESC, 0, true);
				rfb.writeKeyEvent(CTRL, 0, false);
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

				for(int i = 0; i < name.length(); i++){
					int keyCode = name.charAt(i);
					rfb.writeKeyEvent(keyCode, 0, true);
				}
				rfb.writeKeyEvent(ENTER, 0, true);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} 
		}
	}

	public void closeApplication(int x, int y){
		try {
			rfb.writePointerEvent(x, y, 0, 1);
			rfb.writePointerEvent(x, y, 0, 0);
			rfb.writeKeyEvent(ENTER, 0, true);
			rfb.writeKeyEvent(ENTER, 0, true);
			rfb.writeKeyEvent(ENTER, 0, true);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

